﻿using System.IO;
using System.Security.Cryptography;

namespace DotNetCrypt
{
    /// <summary>
    /// A generic class for calculating HMAC message authentication codes
    /// using a particular implementation of <see cref="HashAlgorithm"/>.
    /// This class cannot be inherited.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This class provides duplicate functionality to the HMAC implementations
    /// in the <see cref="System.Security.Cryptography"/> namespace, because it
    /// is a generic implementation that will accept any
    /// <see cref="HashAlgorithm"/> implementation. When looking for an HMAC
    /// based on, for example, <see cref="SHA256"/>, the following two lines
    /// will provide functionally equivalent alternatives:
    /// </para>
    /// <code lang="cs">
    /// KeyedHashAlgorithm hmac1 = new HMACSHA256();
    /// KeyedHashAlgorithm hmac2 = new HMAC&lt;SHA256Managed&gt;>();
    /// </code>
    /// <para>
    /// In fact, both algorithms will use the same underlying
    /// <see cref="SHA256" /> implementation "under the hood".
    /// </para>
    /// </remarks>
    /// <typeparam name="T">
    /// The type of the hash implementation to use in calculating the
    /// HMAC.
    /// </typeparam>
// ReSharper disable InconsistentNaming
    public sealed class HMAC<T> : KeyedHashAlgorithm where T : HashAlgorithm, new()
// ReSharper restore InconsistentNaming
    {
        private const byte IPAD = 0x36;
        private const byte OPAD = 0x5c;

        private Stream _buffer;

        private readonly T _hashAlgorithm = new T();
        private byte[] _keyAdjusted;
        private byte[] _keyInner;
        private byte[] _keyOuter;

        /// <overloads>
        /// Creates a new <see cref="HMAC" /> instance.
        /// </overloads>
        /// <summary>
        /// Creates a new <see cref="HMAC" /> instance with a randomly
        /// generated key.
        /// </summary>
        public HMAC() : this(null)
        {
        }

        /// <summary>
        /// Creates a new <see cref="HMAC" /> instance with the given key.
        /// </summary>
        /// <param name="key">
        /// The secret key to use.
        /// </param>
        public HMAC(byte[] key)
        {
            HashSizeValue = _hashAlgorithm.HashSize;
            if (key == null) key = Utils.GenerateRandom(DetermineBlockSize());
            Key = key;
            Initialize();
        }

        /// <summary>
        /// Gets or sets the key to use in the hash algorithm.
        /// </summary>
        /// <returns>
        /// The key to use in the hash algorithm.
        /// </returns>
        /// <exception cref="T:System.Security.Cryptography.CryptographicException">
        /// An attempt was made to change the
        /// <see cref="P:System.Security.Cryptography.KeyedHashAlgorithm.Key"/>
        /// property after hashing has begun. 
        /// </exception>
        public override byte[] Key
        {
            get { return base.Key; }
            set
            {
                base.Key = value;
                Initialize();
            }
        }

        /// <summary>
        /// Initializes an implementation of the
        /// <see cref="T:System.Security.Cryptography.HashAlgorithm"/> class.
        /// </summary>
        public override void Initialize()
        {
            _buffer = new MemoryStream();
            Precompute();
        }

        /// <summary>
        /// Routes data written to the
        /// object into the hash algorithm for computing the hash.
        /// </summary>
        /// <param name="array">
        /// The input to compute the hash code for. 
        /// </param>
        /// <param name="ibStart">
        /// The offset into the byte array from which to begin using data. 
        /// </param>
        /// <param name="cbSize">
        /// The number of bytes in the byte array to use as data. 
        /// </param>
        protected override void HashCore(byte[] array, int ibStart, int cbSize)
        {
            _buffer.Write(array, ibStart, cbSize);
        }

        /// <summary>
        /// Finalizes the hash computation after the last data is processed
        /// by the cryptographic stream object.
        /// </summary>
        /// <returns>
        /// The computed hash code.
        /// </returns>
        protected override byte[] HashFinal()
        {
            _buffer.Position = 0;
            _hashAlgorithm.Initialize();
            byte[] intermediateHash = _hashAlgorithm.ComputeHash(_buffer);
            _buffer.Close();
            var secondInput = new byte[intermediateHash.Length + _keyOuter.Length];
            _keyOuter.CopyTo(secondInput, 0);
            intermediateHash.CopyTo(secondInput, _keyOuter.Length);
            _hashAlgorithm.Initialize();
            return _hashAlgorithm.ComputeHash(secondInput);
        }

        private void Precompute()
        {
            int blockSize = DetermineBlockSize();
            byte[] temp;
            if (Key.Length > blockSize)
            {
                _hashAlgorithm.Initialize();
                temp = _hashAlgorithm.ComputeHash(Key);
            }
            else
            {
                temp = Key;
            }
            _keyAdjusted = new byte[blockSize];
            temp.CopyTo(_keyAdjusted, 0);

            _keyInner = new byte[blockSize];
            for (int intLoop = 0; intLoop < blockSize; intLoop++)
            {
                _keyInner[intLoop] = (byte)(_keyAdjusted[intLoop] ^ IPAD);
            }

            _keyOuter = new byte[blockSize];
            for (int intLoop = 0; intLoop < blockSize; intLoop++)
            {
                _keyOuter[intLoop] = (byte)(_keyAdjusted[intLoop] ^ OPAD);
            }
            _buffer.Write(_keyInner, 0, _keyInner.Length);
        }

        private int DetermineBlockSize()
        {
            int blockSize = _hashAlgorithm.InputBlockSize;
            if (blockSize == 1)
            {
                blockSize = (_hashAlgorithm is SHA384) || (_hashAlgorithm is SHA512) ? 0x80 : 0x40;
            }
            return blockSize;
        }
    }
}
